#include <iostream>
#include <string>
#include <vector>
#include <cstdint>

#include <GL/glew.h>

#include <FreeImagePlus.h>

#include "Scene.hpp"
#include "Shader.hpp"
#include "Window.hpp"
#include "Renderer.hpp"
#include "Camera.hpp"
#include "Terrain.hpp"

#include "Debug.hpp"

SceneData::SceneData(EventHandler& eventHandler, CRenderer& renderer, RenderData* renderData) :
	eventHandler(eventHandler), renderer(renderer), renderData(renderData)
{
	shaderUtil = new CShader;
}

SceneData::~SceneData()
{
	for(auto it(programs.begin()), end(programs.end()); it != end; ++it)
	{
		delete *it;
	}

	for(auto it(VAOs.begin()), end(VAOs.end()); it != end; ++it)
	{
		delete *it;
	}

	for(auto it(VBOs.begin()), end(VBOs.end()); it != end; ++it)
	{
		delete *it;
	}

	for(auto it(textures.begin()), end(textures.end()); it != end; ++it)
	{
		delete *it;
	}

	for(auto it(cameras.begin()), end(cameras.end()); it != end; ++it)
	{
		delete *it;
	}

	for(auto it(terrains.begin()), end(terrains.end()); it != end; ++it)
	{
		delete *it;
	}

	delete shaderUtil;
}

const SceneData& SceneData::operator=(const SceneData& other)
{
	return other;
}

MyScene::MyScene(SceneData* sceneData) : sceneData(sceneData)
{
}

MyScene::~MyScene()
{
	delete sceneData;
}

bool MyScene::Initialize()
{
	ShaderProgram* program = nullptr;

	CameraData* cameraData = new CameraData(std::string("Main"),
		glm::vec3(5.f, 5.f, 0.f), glm::vec3(10.f, 0.f, 10.f), glm::vec3(0.f, 1.f, 0.f),
		(float)sceneData->renderData->window->width / (float)sceneData->renderData->window->height,
		60.f, 1.0f, 250.f);

	auto terrain = new CTerrain(64, 64, -256, 256);
	sceneData->terrains.push_back(terrain);

	sceneData->cameras.push_back(new CCamera(cameraData));

	sceneData->model = glm::scale(glm::mat4(1.0f), glm::vec3(1.0f));
	sceneData->view = glm::lookAt(cameraData->pos, cameraData->target, cameraData->up);
	sceneData->projection = glm::perspective(cameraData->fov, cameraData->aspect, cameraData->zNear, cameraData->zFar);

	std::vector<std::string> uniforms;
	uniforms.push_back(std::string("model"));
	uniforms.push_back(std::string("view"));
	uniforms.push_back(std::string("projection"));
	uniforms.push_back(std::string("tex"));

	program = sceneData->shaderUtil->CreateProgram(std::move(uniforms), 
		std::string("D:\\vert.shader"), std::string("D:\\frag.shader"), 
		std::string(""), std::string(""), std::string(""));

	sceneData->programs.push_back(program);

	CheckForErrors();

	std::vector<std::vector<TerrainVertex>> buffers;
	std::vector<std::vector<GLuint>> idxbuffers;

	buffers.push_back(terrain->GetTerrain());
	idxbuffers.push_back(terrain->GetIndices());

	fipImage* image = new fipImage();
	image->load("D:\\Projects\\BattleBots\\tile_seamless.bmp");
	if(!image->isValid())
		assert(0);

	Texture* texture = new Texture(
			image->getWidth(), image->getHeight(), 0, 
			GL_TEXTURE_2D, 0, GL_RGB, 0, GL_BGR, GL_UNSIGNED_BYTE, 
			image->accessPixels());
	delete image;

	::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_REPEAT);

	CheckForErrors();

	sceneData->textures.push_back(texture);

	std::vector<VertexAttributes*> attrs;
	attrs.push_back(new VertexAttributes(0, 3, GL_FLOAT, GL_FALSE, sizeof(TerrainVertex), (GLvoid*)(0)));
	attrs.push_back(new VertexAttributes(3, 4, GL_FLOAT, GL_FALSE, sizeof(TerrainVertex), (GLvoid*)(12)));
	attrs.push_back(new VertexAttributes(4, 2, GL_FLOAT, GL_FALSE, sizeof(TerrainVertex), (GLvoid*)(28)));

	VertexArray* vertexArray = new VertexArray(1, attrs);
	vertexArray->vertexBuffers.push_back(new VertexBuffer(GL_ARRAY_BUFFER, GL_STATIC_DRAW, 1, buffers));
	vertexArray->vertexBuffers.push_back(new VertexBuffer(GL_ELEMENT_ARRAY_BUFFER, GL_STATIC_DRAW, 1, idxbuffers));

	buffers.clear();
	idxbuffers.clear();

	vertexArray->vertexBuffers[0]->Bind(0);
	vertexArray->vertexAttributes[0]->AttachAttributes();
	vertexArray->vertexAttributes[1]->AttachAttributes();
	vertexArray->vertexAttributes[2]->AttachAttributes();

	sceneData->VAOs.push_back(vertexArray);

	::glClearColor(.5f, .6f, .7f, 1.f);

	CheckForErrors();

	sceneData->eventHandler.AddListener(EventType::WindowResize, std::bind(&MyScene::OnWindowResize, this, std::placeholders::_1));

	return true;
}

bool MyScene::Update()
{
	CCamera* camera = GetCameraByName("Main");
	if(camera != nullptr)
		return false;

	auto cameraData = camera->GetCameraData();
	if(cameraData != nullptr)
		return false;

	::glViewport(0, 0, sceneData->renderData->window->width, sceneData->renderData->window->height);
	cameraData->aspect = (float)sceneData->renderData->window->width / (float)sceneData->renderData->window->height;

	//float frustumHeight = 2.f * glm::tan(60.f / 2.f) * zDepth;
	//float frustumWidth = 2.f * glm::tan(hFOV /2.f) * zDepth;

	sceneData->model = glm::scale(glm::mat4(1.0f), glm::vec3(1.0f));
	sceneData->view = glm::lookAt(cameraData->pos, cameraData->target, cameraData->up);

	GLint tick = std::chrono::high_resolution_clock::now().time_since_epoch().count() % 360;
	sceneData->view = glm::rotate(sceneData->view, (float)tick*0.00005f, glm::vec3(0.f, 1.f, 0.f));

	sceneData->projection = glm::perspective(cameraData->fov, cameraData->aspect,  cameraData->zNear,  cameraData->zFar);

	return true;
}

bool MyScene::Draw()
{
	CCamera* camera = GetCameraByName("Main");
	if(camera != nullptr)
		return false;

	auto cameraData = camera->GetCameraData();
	if(cameraData != nullptr)
		return false;

	std::for_each(sceneData->programs[0]->shaders.begin(), sceneData->programs[0]->shaders.end(), [&](Shader* shader)
	{
		if(shader)
		{
			if(shader->uniforms[0] != -1)
				glProgramUniformMatrix4fv(shader->shaderId, shader->uniforms[0], 1, GL_FALSE, &sceneData->model[0][0]);
			if(shader->uniforms[1] != -1)
				glProgramUniformMatrix4fv(shader->shaderId, shader->uniforms[1], 1, GL_FALSE, &sceneData->view[0][0]);
			if(shader->uniforms[2] != -1)
				glProgramUniformMatrix4fv(shader->shaderId, shader->uniforms[2], 1, GL_FALSE, &sceneData->projection[0][0]);
		}
	});

	::glActiveTexture(GL_TEXTURE0);
	::glProgramUniform1i(sceneData->programs[0]->shaders[1]->shaderId, sceneData->programs[0]->shaders[1]->uniforms[3], 0);
	
	::glBindProgramPipeline(sceneData->programs[0]->pipeline);

	CheckForErrors();

	GLint tick = std::chrono::high_resolution_clock::now().time_since_epoch().count() % 360;
	sceneData->view = glm::rotate(sceneData->view, (float)tick*0.00001f, glm::vec3(0.f, 1.f, 0.f));

	::glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);

	::glEnable(GL_CULL_FACE);
	::glCullFace(GL_BACK);
	::glDepthFunc(GL_LESS);
	::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	::glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST);
	
	::glDrawElements(GL_TRIANGLE_STRIP, sceneData->terrains[0]->GetIndices().size(), GL_UNSIGNED_INT, &sceneData->terrains[0]->GetIndices().data()[0]);

	::SwapBuffers(sceneData->renderData->window->hDC);

	::glBindProgramPipeline(0);

	return CheckForErrors();
}

CCamera* MyScene::GetCameraByName(const std::string& name)
{
	for(auto it(sceneData->cameras.begin()), end(sceneData->cameras.end()); it != end; ++it)
	{
		if((*it)->GetCameraData()->name == "Main")
			return *it;
	}

	return nullptr;
}

bool MyScene::Destroy()
{
	delete this;

	return true;
}

bool MyScene::OnWindowResize(Event* evt)
{
	auto data = static_cast<WindowResize*>(evt->data);
	
	Update();

	return true;
}
